/**
 * Projet Multicast Bidois Morgan & Thomas Nunes
 *
 */
package Engine;

import middleware.ClientThread;
import Test.I_RecvMsgHandler;
import static Engine.TypeMessage.ACK;
import static Engine.TypeMessage.NEW_CONNECTION;
import static Engine.TypeMessage.NORMAL;
import static Engine.TypeMessage.SEND_DIRECTORY;
import static Engine.TypeMessage.SEND_ID;
import static Engine.TypeMessage.SOMEONE_LEAVE;
import static Engine.TypeMessage.START_BURST;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import middleware.I_ClientMulticastHandler;
import middleware.I_ServerMulticastHandler;

/**
 * Nio Engine
 */
public class NioEngine implements I_NioEngine {

    protected ServerSocketChannel serverSocketChannel;          // ServerSocket if it's a server
    protected SocketChannel channel2server = null;              //Socket if it's a client
    protected InetAddress address;
    protected Selector selector;
    protected Map< SocketChannel, InfosNio> mapBuffer;          //collection of buffers: one per client connected
    protected ByteBuffer bufferTaille = ByteBuffer.allocate(4); //buffer to stock the size of data received
    protected int port = 5555;
    protected InetAddress host = null;
    protected I_RecvMsgHandler handler;                         // listener for callback
    protected Map< SocketChannel, String> clientList;           //list of all clients connected
    private static final Logger logger = Logger.getLogger(NioEngine.class.getName());
    private boolean alive = true;

    //Differents states to of DataIn method
    protected enum StateReceive {
        // Start to read the lenght, the name and the the data content.

        READ_LENGTH, READ_NAME, READ_CONTENT
    };
    private boolean iAmServer = false;
    private boolean iAmClient = false;
    private int nbGroupClientTotal = 3;
    private int nbGroupClientCurrent = 0;
    private int nbGroupClientServerSocketReady = 0; //Pour savoir le nombre de client qui ont crée leur serveurSocket
    private I_ServerMulticastHandler serverHandlerMulticast;
    private I_ClientMulticastHandler multicastHandler;

    @Override
    public void InitializeAsClient(InetAddress hostAddress, int port, I_RecvMsgHandler handler) throws IOException {
        logger.log(Level.INFO, "Je suis le Client");
        this.address = host;
        this.selector = SelectorProvider.provider().openSelector();
        // non blocking server
        channel2server = SocketChannel.open();
        channel2server.configureBlocking(false);
        channel2server.connect(new InetSocketAddress(port));
        mapBuffer = new HashMap<>();
        //Add the server to map buffer to comunicate with him
        mapBuffer.put(this.channel2server, new InfosNio());
        // be notified when connection
        channel2server.register(selector, SelectionKey.OP_CONNECT);
        this.handler = handler;
    }

    public void InitializeAsClient(InetAddress hostAddress, int port, I_RecvMsgHandler handler, I_ClientMulticastHandler clientHandler) throws IOException {
        InitializeAsClient(hostAddress, port, handler);
        this.multicastHandler = clientHandler;
    }

    @Override
    public void InitializeAsServer(InetAddress hostAddress, int port, I_RecvMsgHandler handler) throws IOException {
        logger.log(Level.INFO, "Je suis le serveur");
        this.address = host;
        this.selector = SelectorProvider.provider().openSelector();
        // non blocking server
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(address, port));

        clientList = new HashMap<>();
        mapBuffer = new HashMap<>();

        this.handler = handler;
        // be notified when connection
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// go to state ACCEPT
    }

    public void InitializeAsServer(InetAddress hostAddress, int port, I_RecvMsgHandler handler, I_ServerMulticastHandler serverHandler) throws IOException {
        this.InitializeAsServer(hostAddress, port, handler);
        this.serverHandlerMulticast = serverHandler;
    }

    public void mainloop() {
        while (alive) {
            SelectionKey localKey = null;
            try {
                this.selector.select();
                logger.log(Level.INFO, "Event envoyé");
                Iterator selectedKeys = this.selector.selectedKeys().iterator();
                while (selectedKeys.hasNext()) {
                    localKey = (SelectionKey) selectedKeys.next();
                    selectedKeys.remove();
                    if (!localKey.isValid()) {
                        logger.log(Level.INFO, "Key invalide");
                        continue;
                    }
                    if (localKey.isConnectable()) {
                        logger.log(Level.INFO, "Event connection");
                        handleConnect(localKey);
                        logger.log(Level.INFO, "fin connect");
                    } else if (localKey.isAcceptable()) {
                        logger.log(Level.INFO, "Event accept");
                        handleAccept(localKey);
                        logger.log(Level.INFO, "Fin accept");
                    } else if (localKey.isReadable()) {
                        logger.log(Level.INFO, "Event read");
                        handleDataIn(localKey);
                        logger.log(Level.INFO, "fin read");
                    } else if (localKey.isWritable()) {
                        logger.log(Level.INFO, "Event write");
                        handleDataOut(localKey);
                        logger.log(Level.INFO, "fin write");
                    }
                }

            } catch (ClosedSelectorException ce) {
                logger.log(Level.SEVERE, "2");
                logger.log(Level.WARNING, "Connection close");
                this.terminate();
                alive = false;
                if (localKey != null && localKey.isValid()) {
                    if (this.clientList != null && this.clientList.containsKey(localKey)) {
                        this.clientList.remove(localKey);
                    }
                } else {
                    logger.log(Level.SEVERE, "localkey invalid");
                }
            } catch (IOException ex) {
                logger.log(Level.SEVERE, "erreur dans mainloop");
                this.terminate();// Si fermeture serveur. 
            }
        }
    }

    @Override
    public void send(byte[] data, TypeMessage type, int idClient) {
        this.send(channel2server, data, type, idClient);
    }

    @Override
    public void send(SocketChannel socketChannel, byte[] data, TypeMessage type, int idClient) {
        Flag flag = new Flag(type);
        ByteBuffer bufferOut;
        //allocate buffer with the size of data (n) + size of data (4) + flag type (2)
        bufferOut = ByteBuffer.allocate(data.length + 4 + 2);
        bufferOut.putInt(data.length);          //add the size
        bufferOut.putShort(flag.getShort());    //add the flag (type of message)
        bufferOut.put(data);                    //Add the the data

        int logicalClock = 1;
        Message message = new Message(flag, logicalClock, idClient, bufferOut); //clock a 1 car pas encore fAIT TODO!
        mapBuffer.get(socketChannel).addMessage2send(message);
        SelectionKey localKey = socketChannel.keyFor(this.selector);
        logger.log(Level.INFO, "****Passage mode write****");

        if (localKey != null) {
            // write mode but we still read new incomming mesages.
            localKey.interestOps(SelectionKey.OP_WRITE & -SelectionKey.OP_READ);
        } else {
            logger.log(Level.WARNING, "-pas de key-");
        }
        //We wake up the selector cause this fonction is call by GUI layer
        this.selector.wakeup();
    }

    /**
     * Server send a message to all clients
     *
     * @param data
     */
    public void send2All(byte[] data, TypeMessage type, int idClient) {
        for (SocketChannel sc : clientList.keySet()) {
            System.out.println("ENVOI A UN CLIENT");
            send(sc, data, type, idClient);
            try {
                Thread.sleep(10);
            } catch (InterruptedException ex) {
                System.out.println("sleep exception.");
            }
        }
    }

    @Override
    public void terminate() {
        try {
            if (channel2server != null) {
                channel2server.close();
            } else {
                logger.log(Level.SEVERE, "impossible a terminate!");
            }
            if (selector != null) {
                selector.close();
            } else {
                logger.log(Level.SEVERE, "imposible a terminate!");
            }
        } catch (IOException ex) {
            logger.log(Level.SEVERE, "imposible a terminate!", ex);
        }
    }

    /**
     * Connect to the server
     *
     * @param key the key coresponding to the socketchannel
     */
    private void handleConnect(SelectionKey key) {
        SocketChannel localSocketChannel = (SocketChannel) key.channel();
        try {
            if (localSocketChannel.finishConnect()) {
                logger.log(Level.INFO, "Connection...");
                logger.log(Level.INFO, "****Passage mode read lenght****");
                key.interestOps(SelectionKey.OP_READ);
            } else {
                logger.log(Level.WARNING, "wait to connect...");
            }
        } catch (IOException ex) {
            logger.log(Level.WARNING, "except handle connect", ex);
        }
//        try {
//            if (channel2server.finishConnect()) {
//                logger.log(Level.INFO, "Connection...");
//                logger.log(Level.INFO, "****Passage mode read lenght****");
//                key.interestOps(SelectionKey.OP_READ);
//            } else {
//                logger.log(Level.WARNING, "wait to connect...");
//            }
//        } catch (IOException ex) {
//            logger.log(Level.WARNING, "except handle connect", ex);
//        }
    }

    /**
     * Accept a connection from a client
     *
     * @param key the key coresponding to the socketchannel
     */
    private void handleAccept(SelectionKey key) {
        try {
            ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
            //accept la connection
            logger.log(Level.INFO, "Attente client");
            SocketChannel localSocketChannel = serverSocket.accept();
            localSocketChannel.configureBlocking(false);
            localSocketChannel.finishConnect();
            //Register
            logger.log(Level.INFO, "****Passage mode read name****");
            if (!clientList.containsKey(localSocketChannel)) {
                logger.log(Level.INFO, "Ajout d'un client");
                clientList.put(localSocketChannel, "");
                mapBuffer.put(localSocketChannel, new InfosNio());

            }
            localSocketChannel.register(selector, SelectionKey.OP_READ);
        } catch (IOException ex) {
            logger.log(Level.WARNING, "except handle accept", ex);
        }
    }

    /**
     * receive data from a socket
     *
     * @param key the key coresponding to the socketchannel
     */
    private void handleDataIn(SelectionKey key) {
        SocketChannel localSocketChannel = (SocketChannel) key.channel();

        //Get infos related to the key:
        ByteBuffer bufferIn = mapBuffer.get(localSocketChannel).getBufferIn();
        StateReceive buffState = mapBuffer.get(localSocketChannel).getBuffState();

        bufferIn.clear();
        bufferTaille.clear();
        int numRead = 0;
        try {
            if (buffState == NioEngine.StateReceive.READ_LENGTH) {//On lit la taille du message et le Flag
                numRead = localSocketChannel.read(bufferTaille);
                if (bufferTaille.remaining() == 0) {
                    bufferTaille.rewind();
                    int tailleMessage = bufferTaille.getInt();
                    buffState = NioEngine.StateReceive.READ_CONTENT;
                    bufferIn = ByteBuffer.allocate(tailleMessage);

                    //Get the flag now (a short int)
                    ByteBuffer bufferFlag = ByteBuffer.allocate(2);
                    bufferFlag.clear();
                    localSocketChannel.read(bufferFlag);
                    bufferFlag.rewind();
                    short shFlag = bufferFlag.getShort();
                    Flag flag = new Flag(shFlag);
                    mapBuffer.get(localSocketChannel).setFlag(flag);
                    System.out.println("flag = " + flag);
                }
            }
            //read the content message
            if (buffState == NioEngine.StateReceive.READ_CONTENT) {
                numRead = localSocketChannel.read(bufferIn);
                if (bufferIn.remaining() == 0) {
                    buffState = NioEngine.StateReceive.READ_LENGTH;
                    String msg = new String(bufferIn.array());
                    if (clientList != null) {
                        if (!msg.startsWith("@WHO@")) {
                            if (msg.startsWith("=")) {
                                String name[] = msg.split("=", 3);
                                //Si nouvelle connection, on actualise les noms.
                                if (clientList.containsKey(localSocketChannel)) {
                                    clientList.remove(localSocketChannel);
                                    clientList.put(localSocketChannel, name[1]);
                                }
                            }
                        } else {
                            // TODO: faire quelque chose du who devenu non fonctionnel avec le multicast
                            //Requete WHO send to server.
                            // send(localSocketChannel, this.who().getBytes(), 1,TypeMessage.NORMAL);
                        }
                    }
                    //it's the le process data / callback to notify !
                    if (!msg.startsWith("@WHO@")) {
                        Flag flag = mapBuffer.get(localSocketChannel).getFlag();
                        Message message = new Message(flag, 1, 1, bufferIn);
                        this.receiveMessage(message);
                    } else {
                        //Affichage de la requête WHO.
                        handler.receivedCB(this.who().getBytes());
                    }
                }
                bufferIn.flip();
            }
            if (numRead == -1) {
                try {
                    key.channel().close();
                    key.cancel();
                } catch (IOException ex) {
                }
            }
        } catch (IOException ex) {
            logger.log(Level.WARNING, "except handle dataIn", ex);
            try {
                key.channel().close();
            } catch (IOException ex1) {
                logger.log(Level.WARNING, "except handle dataIn imposible to close", ex1);
            }
            key.cancel();
        }
    }

    /**
     * Send data to a socket channel
     *
     * @param key the key coresponding to the socketchannel
     */
    private void handleDataOut(SelectionKey key) {
        SocketChannel outSocketChannel = (SocketChannel) key.channel();
        try {
            Message message = mapBuffer.get(outSocketChannel).getMessage2send();
            ByteBuffer bufferOut = message.getContent();
            bufferOut.clear();

            if (bufferOut.hasRemaining()) {
                outSocketChannel.write(bufferOut);

                logger.log(Level.INFO, "Sending message: " + new String(bufferOut.array()));
                //start to read
            }
            if (!bufferOut.hasRemaining()) {
                // if no more message to send, go to read state.
                if (mapBuffer.get(outSocketChannel).isEmptyWaitingList()) {
                    key.interestOps(SelectionKey.OP_READ);
                    logger.log(Level.INFO, "****Passage mode read****");
                }
            }

        } catch (IOException ex) {
            logger.log(Level.WARNING, "except handle dataOut", ex);
            key.cancel();
            try {
                outSocketChannel.close();
            } catch (IOException ex1) {
                logger.log(Level.WARNING, "except handle dataout impossible to close", ex1);
            }
        }
    }

    //@TODO: a supprimer et mettre dans couche midleware
    public String who() {
        String collectionNom = "Les personnes connectés:\n";

        for (String client : this.clientList.values()) {
            collectionNom = collectionNom + "    " + client + "\n";
        }
        return collectionNom;
    }

    //@TODO voir pour les buffer si faut en mettre aussi ...
    public void createSocketServer(InetAddress address, int port) {
        try {
            this.serverSocketChannel = ServerSocketChannel.open();
            this.serverSocketChannel.configureBlocking(false);
            this.serverSocketChannel.socket().bind(new InetSocketAddress(address, port));
        } catch (IOException ex) {
            Logger.getLogger(ClientThread.class.getName()).log(Level.SEVERE, null, ex);
        }
        try {
            this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
        } catch (ClosedChannelException ex) {
            Logger.getLogger(ClientThread.class.getName()).log(Level.SEVERE, null, ex);
        }

        this.clientList = new HashMap<>();
        // System.out.println("SocketServerClient créée : \nAdresse : " + this.serverSocketChannel.socket().getInetAddress()
        //        + " Port : " + this.serverSocketChannel.socket().getLocalPort());
    }

    public void connectClientToAnotherClient(InetAddress adrClientToCo, int portClientToCo, int idClientToCo) {
        try {
            SocketChannel socket = SocketChannel.open();
            socket.configureBlocking(false);
            socket.connect(new InetSocketAddress(adrClientToCo, portClientToCo));
            this.mapBuffer.put(socket, new InfosNio());
            this.clientList.put(socket, "" + idClientToCo);
            // !!!!! HandleConnect se fait par rapport a channel2server donc on change de channel2server a chaque nouvelle connection ..
            //A verifier quand meme ...
//            channel2server = socket;
            socket.register(this.selector, SelectionKey.OP_CONNECT);
            System.out.println("Connection à " + adrClientToCo + ":" + portClientToCo);
        } catch (IOException ex) {
            Logger.getLogger(NioEngine.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Action to do when the nio engine received a message
     *
     * @param message the message received
     */
    public void receiveMessage(Message message) {
        switch (message.getType()) {
            case NORMAL:
                handler.receivedCB(message.getContent().array());
                break;
            case NEW_CONNECTION:
                break;
            case SOMEONE_LEAVE:
                break;
            case START_BURST:
                break;
            case STOP_BURST:
                break;
            case ACK:
                break;
            case SEND_DIRECTORY:
                //Reçu par le client, le serveur lui envoie l'annuaire
                String msg;
                msg = "DEBUG= " + new String(message.getContent().array());
                handler.receivedCB(msg.getBytes());
                multicastHandler.setDirectory(message.getContent());
                multicastHandler.createSocketServer();
                break;
            case SEND_ID:
                //Reçu par le serveur, un client lui envoie son id
                //Just to print a DEBUG message
                msg = "DEBUG= " + new String(message.getContent().array());

                handler.receivedCB(msg.getBytes());
                String strId = new String(message.getContent().array());
                int id = Integer.parseInt(strId);
                serverHandlerMulticast.addClientToDirectory(id);

                nbGroupClientCurrent++;
                if (nbGroupClientCurrent == nbGroupClientTotal) {
                    //If all client are here, we send the directory to the group
                    serverHandlerMulticast.sendDirectory();
                    System.out.println("All clients are here !");
                }
                break;
            case BEGIN_CONNECTION:
                //Le client reçoit le feu vert pour se connecter au groupe
                System.out.println("Client feu vert pour connection");
                multicastHandler.connectToGroup();
                break;
            case CLIENT_SERVERSOCKET_READY:
                //Le serveur reçoit l'information qu'un client à crée sa serversocket
                nbGroupClientServerSocketReady++;
                if (nbGroupClientServerSocketReady == nbGroupClientTotal) {
                    // id à -1 car serveur ???
                    send2All("Tu peux commencer la connexion gros".getBytes(), TypeMessage.BEGIN_CONNECTION, -1);
                }
                break;
        }
    }
}
